package net.avcompris.binding.sax.impl;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static com.google.common.primitives.Primitives.wrap;
import static net.avcompris.binding.impl.TypeUtils.unmarshallValue;
import static net.avcompris.binding.sax.CastUtils.isSimpleType;
import static org.apache.commons.lang3.StringUtils.substringBeforeLast;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Stack;

import net.avcompris.binding.BindConfiguration;
import net.avcompris.binding.annotation.XPath;
import net.avcompris.binding.sax.Unmarshaller;

import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;

import com.avcompris.common.annotation.Nullable;
import com.avcompris.lang.NotImplementedException;

final class DefaultUnmarshaller<T> implements Unmarshaller<T> {

	public DefaultUnmarshaller(
			final BindConfiguration configuration,
			final ClassLoader classLoader,
			final Class<T> clazz) {

		nonNullArgument(configuration, "configuration");
		this.classLoader = nonNullArgument(classLoader, "classLoader");
		this.clazz = nonNullArgument(clazz, "clazz");

		final XPath xpathAnnotation = clazz.getAnnotation(XPath.class);

		if (xpathAnnotation == null) {
			throw new IllegalArgumentException(
					"Class should be annotated with @XPath: " + clazz.getName());
		}

		final String pattern = xpathAnnotation.value();

		//TODO throw new IllegalArgumentException() when there are other flags
		//TODO than XPath.value(), for instance XPath.function()

		configure(pattern, clazz);
	}

	private void addElementPattern(final String pattern,
			final ElementUnmarshall elementUnmarshall) {

		nonNullArgument(pattern, "pattern");
		nonNullArgument(elementUnmarshall, "elementUnmarshall");

		Collection<ElementUnmarshall> elementUnmarshalls = elementPatterns
				.get(pattern);

		if (elementUnmarshalls == null) {

			elementUnmarshalls = new HashSet<ElementUnmarshall>();

			elementPatterns.put(pattern, elementUnmarshalls);
		}

		elementUnmarshalls.add(elementUnmarshall);
	}

	private void configure(final String context, final Class<?> clazz) {

		nonNullArgument(context, "context");
		nonNullArgument(clazz, "clazz");

		final ElementUnmarshall elementUnmarshall = new ElementUnmarshall(clazz);

		addElementPattern(context, elementUnmarshall);

		for (final Method method : clazz.getDeclaredMethods()) {

			final XPath xpathAnnotation = method.getAnnotation(XPath.class);

			if (xpathAnnotation == null) {
				continue;
			}

			final String expression = xpathAnnotation.value();

			final Class<?> returnType = method.getReturnType();

			if (expression.startsWith("@")) {

				final String attributeName = expression.substring(1);

				elementUnmarshall.addAttributeMethod(attributeName, method);

			} else {

				final String pattern = context + "/" + expression;

				final boolean array = returnType.isArray();

				final Class<?> type = array ? returnType.getComponentType()
						: returnType;

				if (isSimpleType(wrap(type))) {

					elementUnmarshall.addCharactersMethod(pattern, method);

				} else {

					elementUnmarshall.addChildMethod(pattern, method);

					configure(pattern, type);
				}
			}
		}
	}

	private final Map<String, Collection<ElementUnmarshall>> elementPatterns = new HashMap<String, Collection<ElementUnmarshall>>();

	private final Class<T> clazz;

	private final ClassLoader classLoader;

	private final Stack<ValuesHolder> valuesHolders = new Stack<ValuesHolder>();

	private Object result = null;

	private ValuesHolder currentHolder;

	private String currentPath = null;

	private final StringBuffer sb = new StringBuffer();

	@Override
	public T getResult() {

		return clazz.cast(result);
	}

	@Override
	public void setDocumentLocator(@Nullable final Locator locator) {

		//this.locator=locator;
	}

	//private Locator locator;

	@Override
	public void startDocument() throws SAXException {

		currentPath = "";

		sb.setLength(0);
	}

	@Override
	public void endDocument() throws SAXException {

		// do nothing
	}

	@Override
	public void startElement(@Nullable final String namespaceURI,
			final String localName, final String qName, final Attributes attrs) throws SAXException {

		nonNullArgument(localName, "localName");
		nonNullArgument(qName, "qName");
		nonNullArgument(attrs, "attrs");

		//consumeChars();

		currentPath += "/" + localName;

		if (currentHolder != null
				&& currentHolder.getElementUnmarshall().getCharactersMethod(
						currentPath) != null) {

			sb.setLength(0);
		}

		final Collection<ElementUnmarshall> elementUnmarshalls = elementPatterns
				.get(currentPath);

		if (elementUnmarshalls != null) {

			if (elementUnmarshalls.size() != 1) {
				throw new NotImplementedException(
						"elementUnmarshalls.size() != 1: "
								+ elementUnmarshalls.size());
			}

			final ElementUnmarshall elementUnmarshall = elementUnmarshalls
					.iterator().next();

			final ValuesHolder valuesHolder = new ValuesHolder(classLoader,
					elementUnmarshall);

			final Object proxy = valuesHolder.getObjectProxy();

			if (currentHolder == null) {

				result = proxy;
			}

			currentHolder = valuesHolder;

			valuesHolders.push(valuesHolder);

			final int attributeCount = attrs.getLength();

			for (int i = 0; i < attributeCount; ++i) {

				final String attributeName = attrs.getLocalName(i);
				final String attributeValue = attrs.getValue(i);

				final Method method = elementUnmarshall
						.getAttributeMethod(attributeName);

				if (method != null) {

					final Class<?> type = method.getReturnType();

					valuesHolder.setValue(method, unmarshallValue(wrap(type),
							attributeValue));
				}
			}
		}
	}

	@Override
	public void endElement(@Nullable final String namespaceURI,
			final String localName, final String qName) throws SAXException {

		nonNullArgument(localName, "localName");
		nonNullArgument(qName, "qName");

		consumeChars();

		final String parentPath = substringBeforeLast(currentPath, "/");

		final Collection<ElementUnmarshall> elementUnmarshalls = elementPatterns
				.get(currentPath);

		if (elementUnmarshalls != null) {

			valuesHolders.pop();

			ValuesHolder nextCurrentHolder = currentHolder;

			for (final ValuesHolder ancestorHolder : valuesHolders) {

				final ElementUnmarshall ancestorUnmarshall = ancestorHolder
						.getElementUnmarshall();

				final Method method = ancestorUnmarshall
						.getChildMethod(currentPath);

				if (method == null) {
					continue;
				}

				ancestorHolder.addValueToArray(method, currentHolder
						.getObjectProxy());

				nextCurrentHolder = ancestorHolder;
			}

			currentHolder = nextCurrentHolder;
		}

		currentPath = parentPath;
	}

	@Override
	public void characters(final char[] ch, final int start, final int length) throws SAXException {

		nonNullArgument(ch, "chars");

		sb.append(ch, start, length);
	}

	@Override
	public void ignorableWhitespace(final char[] ch, final int start,
			final int length) throws SAXException {

		nonNullArgument(ch, "chars");
	}

	private void consumeChars() {

		if (currentHolder != null) {

			final Method method = currentHolder.getElementUnmarshall()
					.getCharactersMethod(currentPath);

			if (method != null) {

				final Class<?> returnType = method.getReturnType();

				if (!returnType.isArray()) {

					if (!currentHolder.hasValue(method)) {

						currentHolder.setValue(method, unmarshallValue(
								wrap(returnType), sb.toString()));
					}

					sb.setLength(0);

				} else {

					currentHolder
							.addValueToArray(method, unmarshallValue(
									wrap(returnType.getComponentType()), sb
											.toString()));

					sb.setLength(0);
				}
			}
		}
	}

	@Override
	public void startPrefixMapping(final String prefix, final String uri) throws SAXException {

		nonNullArgument(prefix, "prefix");
		nonNullArgument(uri, "uri");
	}

	@Override
	public void endPrefixMapping(final String prefix) throws SAXException {

		nonNullArgument(prefix, "prefix");
	}

	@Override
	public void processingInstruction(final String target, final String data) throws SAXException {

		nonNullArgument(target, "target");
		nonNullArgument(data, "data");
	}

	@Override
	public void skippedEntity(final String name) throws SAXException {

		nonNullArgument(name, "name");
	}
}
